package com.evzgaga.evzrecon.xmlrpc.client;

import com.evzgaga.evzrecon.event.ChatCommandEvent;
import java.util.List;
import com.evzgaga.evzrecon.event.ServerCallbackEvent;
import org.apache.xmlrpc.client.XmlRpcClientConfig;
import org.apache.xmlrpc.util.SAXParsers;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import com.evzgaga.evzrecon.io.LittleEndianDataInputStream;
import com.evzgaga.evzrecon.util.CallbackInfo;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.common.XmlRpcStreamConfig;
import org.apache.xmlrpc.parser.XmlRpcRequestParser;
import org.xml.sax.InputSource;
import static com.evzgaga.evzrecon.util.LittleEndianConverter.*;

/**
 *
 * @author Bastien
 */
public class GbxRemoteConnection {
    private Socket sock;
    private XmlRpcClient client;
    private static final Logger logger = Logger.getLogger(GbxRemoteConnection.class.getName());
    private int gbxRemoteVersion;
    private int requestHandle = 0x80000000;
    private Map<Integer, byte[]> serverResponses = new HashMap<Integer, byte[]>();
    private volatile boolean mustReadResponses = true;
    private XmlRpcStreamConfig streamConfig;
    private Map<String, List<CallbackInfo>> serverCallbackHandlers = new HashMap<String, List<CallbackInfo>>();
    private Map<String, List<CallbackInfo>> chatCommandHandlers = new HashMap<String, List<CallbackInfo>>();

    public GbxRemoteConnection(String hostName, int port) throws IOException {
        sock = new Socket(hostName, port);
        connect();
        readGbxRemoteVersion();
        waitForResponses();
        enableCallbacks();
    }
    
    protected final void connect() {
        client = new XmlRpcClient();
        client.setTransportFactory(new XmlRpcRawSocketTransportFactory(client));
        client.setXmlWriterFactory(new GbxXmlWriterFactory());
        streamConfig = new XmlRpcRawSocketClientConfig(this);
        client.setConfig((XmlRpcClientConfig) streamConfig);
    }

    private void readGbxRemoteVersion() throws IOException {
        String gbxRemote = new String(readGbxMessage().data);

        gbxRemoteVersion = (gbxRemote.equals("GBXRemote 1")) ? 1 : 2;

        logger.log(Level.INFO, "Connected to remote server, protocol version is {0} ({1})",
                new Object[]{gbxRemoteVersion, gbxRemote});
    }

    private GbxResponseDTO readGbxMessage() throws IOException {
        LittleEndianDataInputStream inputStream = LittleEndianDataInputStream.wrap(sock.getInputStream());

        int messageLength = inputStream.readInt();
        int handle = 0;

        if (gbxRemoteVersion > 1) {
            handle = inputStream.readInt();
        }

        byte[] message = new byte[messageLength];
        inputStream.read(message);

        logger.log(Level.FINE, "Read handle {0} ({1} bytes). {2} bytes still available in the stream",
                new Object[]{Integer.toHexString(handle), messageLength, inputStream.available()});

        return new GbxResponseDTO(handle, message);
    }

    public Object callRemoteMethod(String methodName, Object... args) {
        Object returnedObject = "";

        try {
            returnedObject = client.execute(methodName, args);
        } catch (XmlRpcException e) {
            logger.log(Level.SEVERE, "Could not call remote method " + methodName, e);
        }
        
        return returnedObject;
    }

    private void enableCallbacks() {
        Boolean callbacksEnabled = (Boolean) callRemoteMethod("EnableCallbacks", true);
        logger.log(Level.INFO, "Callbacks enabled: {0}", callbacksEnabled.booleanValue());
    }
    
    public void closeConnection() throws IOException {
        sock.close();
        mustReadResponses = false;
    }
    
    protected InputStream getServerInputStream() throws IOException {
        return sock.getInputStream();
    }
    
    protected OutputStream getServerOutputStream() throws IOException {
        return sock.getOutputStream();
    }

    protected int sendGbxRequest(String documentContent) {
        try {
            final int length = documentContent.length();

            logger.log(Level.FINE, "Sending XML-RPC request of length {0} using handle {1}: {2}",
                    new Object[]{length, Integer.toHexString(requestHandle), documentContent});

            OutputStream outputStream = sock.getOutputStream();
            outputStream.write(toLittleEndian(length));
            outputStream.write(toLittleEndian(requestHandle));
            outputStream.write(documentContent.getBytes());
//            outputStreamWriter.write(getWriter().toString());
            outputStream.flush();
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "An error occured while sending the GBX request", ex);
        }

        int returnedHandle = requestHandle;
        requestHandle++;
        
        return returnedHandle;
    }

    protected byte[] getGbxResponse(int requestHandle) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            if (serverResponses.containsKey(requestHandle)) {
                return serverResponses.get(requestHandle);
            } else {
                logger.log(Level.INFO, "Response for handle {0} not yet received, waiting 500ms before retrying",
                        Integer.toHexString(requestHandle));
                Thread.sleep(500);
            }
        }
        
        logger.log(Level.INFO, "Response for handle {0} was too long to receive, aborting!", requestHandle);
        return new byte[0];
    }

    private void waitForResponses() {
        new Thread(new ResponseHandler()).start();
        // TODO: store thread in field to call th.stop() in closeConnection()?
    }
    
    private void callbackReceived(byte[] callbackContent) {
        try {
            XmlRpcRequestParser parser = parseXmlCallback(callbackContent);
            ServerCallbackEvent event = new ServerCallbackEvent(parser.getMethodName(), parser.getParams());
            Logger.getLogger(GbxRemoteConnection.class.getName()).log(Level.INFO, "Received callback {0} with params {1}",
                    new Object[] {event.callbackName, event.callbackParams});
            fireServerCallbackEvent(event);
        } catch (Exception ex) {
            Logger.getLogger(GbxRemoteConnection.class.getName()).log(Level.SEVERE,
                    "Could not parse or dispatch server callback", ex);
        }
    }
    
    private XmlRpcRequestParser parseXmlCallback(byte[] callbackContent) throws SAXException, IOException, XmlRpcException {
        InputSource input = new InputSource(new ByteArrayInputStream(callbackContent));
        XMLReader reader = SAXParsers.newXMLReader();
        XmlRpcRequestParser parser = new XmlRpcRequestParser(streamConfig, client.getTypeFactory());
        reader.setContentHandler(parser);
        reader.parse(input);
        return parser;
    }

    private void fireServerCallbackEvent(ServerCallbackEvent event) {
        List<CallbackInfo> callbacks = serverCallbackHandlers.get(event.callbackName);
        
        if (callbacks == null) {
            return;
        }

        for (CallbackInfo callback : callbacks) {
            try {
                callback.method.invoke(callback.instance, event);
            } catch (Exception ex) {
                Logger.getLogger(GbxRemoteConnection.class.getName()).log(Level.SEVERE, "Could not call handler for callback", ex);
            }
        }
    }

    public void fireChatCommandEvent(ChatCommandEvent event) {
        List<CallbackInfo> callbacks = chatCommandHandlers.get(event.commandName);
        
        if (callbacks == null) {
            return;
        }

        for (CallbackInfo callback : callbacks) {
            try {
                callback.method.invoke(callback.instance, event);
            } catch (Exception ex) {
                Logger.getLogger(GbxRemoteConnection.class.getName()).log(Level.SEVERE, "Could not call handler for callback", ex);
            }
        }
    }

    public void addServerCallbackListener(String callbackName, CallbackInfo callbackInfo) {
        registerListener(callbackName, callbackInfo, serverCallbackHandlers);
    }

    private void registerListener(String callbackName, CallbackInfo callbackInfo, Map<String, List<CallbackInfo>> listeners) {
        if (!listeners.containsKey(callbackName)) {
            listeners.put(callbackName, new ArrayList<CallbackInfo>());
        }
        
        listeners.get(callbackName).add(callbackInfo);
    }

    public void addAllServerCallbackListeners(Map<String, CallbackInfo> callbacks) {
        for (String callback : callbacks.keySet()) {
            addServerCallbackListener(callback, callbacks.get(callback));
        }
    }

    public void addChatCommandListener(String callbackName, CallbackInfo callbackInfo) {
        registerListener(callbackName, callbackInfo, chatCommandHandlers);
    }

    public void addAllChatCommandListeners(Map<String, CallbackInfo> callbacks) {
        for (String callback : callbacks.keySet()) {
            addChatCommandListener(callback, callbacks.get(callback));
        }
    }

    private class ResponseHandler implements Runnable {

        public void run() {
            while (mustReadResponses) {
                readResponse();
            }
        }
        
        private void readResponse() {
            try {
                GbxResponseDTO response = readGbxMessage();
                
                if (response.isCallback()) {
                    callbackReceived(response.data);
                } else {
                    serverResponses.put(response.handle, response.data);
                }
            } catch (IOException ex) {
                Logger.getLogger(GbxRemoteConnection.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    
    private class GbxResponseDTO {
        public final int handle;
        public final byte[] data;

        public GbxResponseDTO(int handle, byte[] data) {
            this.handle = handle;
            this.data = data;
        }
        
        public boolean isCallback() {
            return ((handle & 0x80000000) == 0);
        }
    }
}
